// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: jshin

#pragma once

#include <string>
#include <string_view>

#include <rapidjson/document.h>
#include <rapidjson/rapidjson.h>

#include "coin/proto/coin_market_enums.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "coin2/exchange/base/feed_util/trade.h"

namespace impl {
using coin2::exchange::base::market::ExchangeType;
using coin2::exchange::base::market::MarketType;
using namespace std::literals;

class GateioFeedUpdateParser {
 public:
  static void ParseBbo(const rapidjson::Value& result, BookBuilder* book_builder_) {
    auto ts = stoull(result["t"].GetString()) * 1'000'000LL;  // ms -> ns
    auto tx = book_builder_->NewTransaction(ts);
    tx.Clear();

    if (result.HasMember("a") && (result["a"].GetStringLength() > 0)) {
      tx.UpdateAsk0(
          stod(result["a"].GetString()),
          stod(result["A"].GetString()));
    }

    if (result.HasMember("b") && (result["b"].GetStringLength() > 0)) {
      tx.UpdateBid0(
          stod(result["b"].GetString()),
          stod(result["B"].GetString()));
    }
  }
  static bool ParseBookUpdate(
      const rapidjson::Value& result,
      BookBuilder* book_builder) {
    if (book_builder->BidSize() == 0 || book_builder->AskSize() == 0){
      return false;
    }
    auto ts = stoull(result["t"].GetString()) * 1'000'000LL;  // ms -> ns
    auto tx = book_builder->NewTransaction(ts);

    if (result.HasMember("a") && (!result["a"].Empty())) {
      for (const auto& pq : result["a"].GetArray()) {
        tx.UpdateAsk(
            stod(pq[0].GetString()),
            stod(pq[1].GetString()));
      }
    }
    if (result.HasMember("b") && (!result["b"].Empty())) {
      for (const auto& pq : result["b"].GetArray()) {
        tx.UpdateBid(
            stod(pq[0].GetString()),
            stod(pq[1].GetString()));
      }
    }
    return true;
  }
  static void ParseOrderBookUpdate(
      const rapidjson::Value& result,
      BookBuilder* book_builder) {
    auto ts = stoull(result["t"].GetString()) * 1'000'000LL;  // ms -> ns
    auto tx = book_builder->NewTransaction(ts);

    if (result.HasMember("a") && (!result["a"].IsNull())) {
      for (const auto& pq : result["a"].GetArray()) {
        tx.UpdateAsk(
            stod(pq["p"].GetString()),
            stod(pq["s"].GetString()));
      }
    }
    if (result.HasMember("b") && (!result["b"].IsNull())) {
      for (const auto& pq : result["b"].GetArray()) {
        tx.UpdateBid(
            stod(pq["p"].GetString()),
            stod(pq["s"].GetString()));
      }
    }
  }
static bool ParseBookSnapshot(
      const rapidjson::Value& result,
      BookBuilder* book_builder) {
    if (book_builder->BidSize() > 0 && book_builder->AskSize() > 0) {
      return false;
    }

    auto ts = stoull(result["t"].GetString()) * 1'000'000LL;  // ms -> ns
    auto tx = book_builder->NewTransaction(ts);
    tx.Clear();

    if (result.HasMember("asks") && (!result["asks"].IsNull())) {
      for (const auto& pq : result["asks"].GetArray()) {
        tx.UpdateAsk(
            stod(pq[0].GetString()),
            stod(pq[1].GetString()));
      }
    }
    if (result.HasMember("bids") && (!result["bids"].IsNull())) {
      for (const auto& pq : result["bids"].GetArray()) {
        tx.UpdateBid(
            stod(pq[0].GetString()),
            stod(pq[1].GetString()));
      }
    }
    return true;
  }
  static void ParseOrderBook(
      const rapidjson::Value& result,
      BookBuilder* book_builder) {
    auto ts = stoull(result["t"].GetString()) * 1'000'000LL;  // ms -> ns
    auto tx = book_builder->NewTransaction(ts);
    tx.Clear();

    if (result.HasMember("asks") && (!result["asks"].IsNull())) {
      for (const auto& pq : result["asks"].GetArray()) {
        tx.UpdateAsk(
            stod(pq["p"].GetString()),
            stod(pq["s"].GetString()));
      }
    }
    if (result.HasMember("bids") && (!result["bids"].IsNull())) {
      for (const auto& pq : result["bids"].GetArray()) {
        tx.UpdateBid(
            stod(pq["p"].GetString()),
            stod(pq["s"].GetString()));
      }
    }
  }
  static void ParseTrade(const rapidjson::Value& trade_json, Trade* trade) {
    // trade_json->timestamp is reused
    trade->timestamp = (int64_t)(stod(trade_json["create_time_ms"].GetString()) * 1'000'000LL);
    trade->price = stod(trade_json["price"].GetString());
    trade->fill_qty = stod(trade_json["amount"].GetString());
    trade->side = ParseTradeSide(trade_json["side"].GetString());
  }
  static void ParseFuturesTrade(const rapidjson::Value& trade_json, Trade* trade) {
    // trade_json->timestamp is reused
    if (trade_json.HasMember("create_time_ms")) {
      trade->timestamp = (int64_t)(stoll(trade_json["create_time_ms"].GetString()) * 1'000'000LL);
    } else {
      trade->timestamp = (int64_t)(stoll(trade_json["time_ms"].GetString()) * 1'000'000LL);
    }
    trade->price = stod(trade_json["price"].GetString());
    double size = stod(trade_json["size"].GetString());
    trade->fill_qty = abs(size);
    trade->side = ParseFuturesTradeSide(size);
  }
  static coin2::exchange::feed::TradeSide ParseTradeSide(std::string_view side) {
    if (side == "buy") {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else if (side == "sell") {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else {
      throw std::runtime_error("[Gateio] unknown side: " + std::string(side));
    }
  }
  static coin2::exchange::feed::TradeSide ParseFuturesTradeSide(double size) {
    if (size > 0) {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else if (size < 0) {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else {
      throw std::runtime_error("[Gateio] unknown side: " + std::to_string(size));
    }
  }
  static fastfeed::proto::KlineInterval ParseKlineInterval(const std::string& interval) {
    static std::map<std::string, fastfeed::proto::KlineInterval> interval_map = {
      {"1m", fastfeed::proto::KLINE_INTERVAL_1MIN},
      {"3m", fastfeed::proto::KLINE_INTERVAL_3MIN},
      {"5m", fastfeed::proto::KLINE_INTERVAL_5MIN},
      {"15m", fastfeed::proto::KLINE_INTERVAL_15MIN},
      {"30m", fastfeed::proto::KLINE_INTERVAL_30MIN},
      {"1h", fastfeed::proto::KLINE_INTERVAL_1HOUR},
      {"1d", fastfeed::proto::KLINE_INTERVAL_1DAY},
    };
    auto iter = interval_map.find(interval);
    if (iter == interval_map.end()) {
      
      return fastfeed::proto::KLINE_INTERVAL_INVALID;
    }

    return iter->second;
  }

  static bool Parse1Kline(
      const rapidjson::Value& kline_data,
      Klines* klines,
      int64_t timestamp,
      fastfeed::proto::KlineInterval interval) {
    CHECK(kline_data.Size() == 6 || kline_data.Size() == 7);
    int64_t open_timestamp = stoll(kline_data[0].GetString()) * 1'000'000'000LL;
    auto result = klines->AddKline(open_timestamp);
    if (!result) return false;

    Kline* kline;
    bool freezed;
    std::tie(kline, freezed) = result.value();

    kline->open = stod(kline_data[5].GetString());
    kline->high = stod(kline_data[3].GetString());
    kline->low = stod(kline_data[4].GetString());
    kline->close = stod(kline_data[2].GetString());
    kline->turnover = stod(kline_data[1].GetString());
    if(kline_data.Size() == 6) {
      kline->volume = kline->turnover / kline->close;
    } else if (kline_data.Size() == 7) {
      kline->volume = stod(kline_data[6].GetString());;
    }
    kline->timestamp = timestamp;
    kline->interval = interval;
    return freezed;
  }
};

}  // namespace impl

using impl::GateioFeedUpdateParser;
